<?php

declare(strict_types=1);

namespace core;

use InvalidArgumentException;

/**
 * Class Validator
 *
 * 单文件数据验证器，支持：
 * - 规则字符串：'required|between:3,32'
 * - 全局自定义错误：['required|between:3,32', '通用错误']
 * - 每条规则自定义错误：['required' => '必填', 'between:3,32' => '长度3-32']
 */
final class Validator
{
    private const DEFAULT_ERROR_MESSAGES = [
        'required'  => '字段 {field} 不能为空',
        'optional'  => '',
        'string'    => '字段 {field} 必须是字符串',
        'integer'   => '字段 {field} 必须是整数',
        'numeric'   => '字段 {field} 必须是数字',
        'float'     => '字段 {field} 必须是浮点数',
        'alpha'     => '字段 {field} 只能包含字母',
        'alpha_num' => '字段 {field} 只能包含字母和数字',
        'email'     => '字段 {field} 必须是有效的邮箱地址',
        'url'       => '字段 {field} 必须是有效的URL',
        'phone'     => '字段 {field} 不是一个有效的手机号码',
        'same'      => '字段 {field} 必须与 {param} 相同',
        'min'       => '字段 {field} 的长度不能小于 {param}',
        'max'       => '字段 {field} 的长度不能超过 {param}',
        'between'   => '字段 {field} 的值必须在 {param} 和 {param2} 之间',
        'gt'        => '字段 {field} 的值必须大于 {param}',
        'gte'       => '字段 {field} 的值必须大于或等于 {param}',
        'lt'        => '字段 {field} 的值必须小于 {param}',
        'lte'       => '字段 {field} 的值必须小于或等于 {param}',
    ];

    private ?string $error = null;

    public function __construct(
        private array $data,
        private array $validationRules,
        private bool $checkCsrfToken = true
    ) {
    }

    public function validate(): bool
    {
        if ($this->checkCsrfToken && !$this->validateCsrfToken()) {
            $this->error = '无效的请求或页面已过期，请刷新重试。';
            return false;
        }

        foreach ($this->validationRules as $field => $ruleConfig) {
            // 获取规则级自定义错误表
            $ruleErrors = $this->extractRuleErrors($ruleConfig);

            // 遍历规则
            foreach ($ruleErrors as $rule => $_) {
                if ($rule === '*') continue; // 全局错误跳过

                [$ruleName, $params] = $this->parseRule($rule);
                $method = $this->mapRuleToMethod($ruleName);

                if (!method_exists($this, $method)) {
                    throw new InvalidArgumentException("验证规则 '{$ruleName}' 不存在。");
                }

                $value = $this->data[$field] ?? null;
                if (is_string($value)) {
                    $value = trim($value);
                }

                // optional 特殊处理
                if ($ruleName === 'optional' && ($value === null || $value === '')) {
                    break; // 跳过后续规则
                }

                if (!$this->$method($value, $field, ...$params)) {
                    $this->error = $this->buildErrorMessage($ruleName, $field, $params, $ruleErrors);
                    return false;
                }
            }
        }

        return true;
    }

    public function getError(): ?string
    {
        return $this->error;
    }

    /* --------------------- CSRF --------------------- */

    private function validateCsrfToken(): bool
    {
        $token        = $this->data['csrf_token'] ?? '';
        $sessionToken = $_SESSION['csrf_token'] ?? '';

        if (!$token || !$sessionToken) return false;

        unset($_SESSION['csrf_token']);

        return hash_equals((string)$sessionToken, (string)$token);
    }

    /* --------------------- 工具方法 --------------------- */

    private function parseRule(string $rule): array
    {
        if (str_contains($rule, ':')) {
            [$name, $paramStr] = explode(':', $rule, 2);
            return [trim($name), array_map('trim', explode(',', $paramStr))];
        }
        return [$rule, []];
    }

    private function mapRuleToMethod(string $rule): string
    {
        $map = [
            'required'  => 'validateRequired',
            'optional'  => 'validateOptional',
            'string'    => 'validateString',
            'integer'   => 'validateInteger',
            'numeric'   => 'validateNumeric',
            'float'     => 'validateFloat',
            'alpha'     => 'validateAlpha',
            'alpha_num' => 'validateAlphaNum',
            'email'     => 'validateEmail',
            'url'       => 'validateUrl',
            'phone'     => 'validatePhone',
            'same'      => 'validateSame',
            'min'       => 'validateMin',
            'max'       => 'validateMax',
            'between'   => 'validateBetween',
            'gt'        => 'validateGt',
            'gte'       => 'validateGte',
            'lt'        => 'validateLt',
            'lte'       => 'validateLte',
        ];
        return $map[$rule] ?? 'validate' . ucfirst($rule);
    }

    private function extractRuleErrors(array $ruleConfig): array
    {
        $ruleErrors = [];

        if (isset($ruleConfig[0]) && is_string($ruleConfig[0])) {
            $globalRuleString = $ruleConfig[0];
            if (isset($ruleConfig[1]) && is_string($ruleConfig[1])) {
                $ruleErrors['*'] = $ruleConfig[1];
            }
            foreach (explode('|', $globalRuleString) as $r) {
                $r = trim($r);
                if ($r !== '') $ruleErrors[$r] = $ruleErrors[$r] ?? null;
            }
        }

        foreach ($ruleConfig as $key => $value) {
            if (is_string($key)) {
                $ruleErrors[$key] = $value;
            }
        }

        return $ruleErrors;
    }

    private function buildErrorMessage(string $rule, string $field, array $params, array $ruleErrors): string
    {
        if (isset($ruleErrors[$rule]) && $ruleErrors[$rule] !== null) {
            return str_replace(['{field}', '{param}', '{param2}'], [$field, $params[0] ?? '', $params[1] ?? ''], $ruleErrors[$rule]);
        }
        if (isset($ruleErrors['*'])) {
            return $ruleErrors['*'];
        }
        $msg = self::DEFAULT_ERROR_MESSAGES[$rule] ?? "字段 {$field} 验证失败。";
        return str_replace(['{field}', '{param}', '{param2}'], [$field, $params[0] ?? '', $params[1] ?? ''], $msg);
    }

    /* --------------------- 验证方法 --------------------- */

    private function validateOptional(mixed $value, string $field): bool
    {
        return true;
    }
    private function validateRequired(mixed $value, string $field): bool
    {
        return $value !== null && (!is_string($value) || trim((string)$value) !== '');
    }
    private function validateString(mixed $value, string $field): bool
    {
        return is_string($value);
    }
    private function validateInteger(mixed $value, string $field): bool
    {
        return filter_var($value, FILTER_VALIDATE_INT) !== false;
    }
    private function validateNumeric(mixed $value, string $field): bool
    {
        return is_numeric($value);
    }
    private function validateFloat(mixed $value, string $field): bool
    {
        return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
    }
    private function validateAlpha(mixed $value, string $field): bool
    {
        return is_string($value) && ctype_alpha($value);
    }
    private function validateAlphaNum(mixed $value, string $field): bool
    {
        return is_string($value) && ctype_alnum($value);
    }
    private function validateEmail(mixed $value, string $field): bool
    {
        return is_string($value) && filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
    }
    private function validateUrl(mixed $value, string $field): bool
    {
        return is_string($value) && filter_var($value, FILTER_VALIDATE_URL) !== false;
    }
    private function validatePhone(mixed $value, string $field): bool
    {
        return is_string($value) && preg_match('/^1[3-9]\d{9}$/', $value) === 1;
    }
    private function validateSame(mixed $value, string $field, string $otherField): bool
    {
        return $value === ($this->data[$otherField] ?? null);
    }
    private function validateMin(mixed $value, string $field, string $min): bool
    {
        return is_string($value) ? mb_strlen($value, 'UTF-8') >= (int)$min : (float)$value >= (float)$min;
    }
    private function validateMax(mixed $value, string $field, string $max): bool
    {
        return is_string($value) ? mb_strlen($value, 'UTF-8') <= (int)$max : (float)$value <= (float)$max;
    }
    private function validateBetween(mixed $value, string $field, string $min, string $max): bool
    {
        if (!is_numeric($value)) return false;
        $v = (float)$value;
        return $v >= (float)$min && $v <= (float)$max;
    }
    private function validateGt(mixed $value, string $field, string $limit): bool
    {
        return is_numeric($value) && (float)$value > (float)$limit;
    }
    private function validateGte(mixed $value, string $field, string $limit): bool
    {
        return is_numeric($value) && (float)$value >= (float)$limit;
    }
    private function validateLt(mixed $value, string $field, string $limit): bool
    {
        return is_numeric($value) && (float)$value < (float)$limit;
    }
    private function validateLte(mixed $value, string $field, string $limit): bool
    {
        return is_numeric($value) && (float)$value <= (float)$limit;
    }
}
